package com.munisense.webservices.javaclient;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.xml.sax.SAXException;

/**
 * Helper class that allows the service classes to communicate with the
 * webservice.
 * 
 * @param <T>
 *            Declared Type of the data that is requested
 */
public class MuniHTTPClient<T> {

	private final Marshaller marshaller;
	private final Unmarshaller unmarshaller;

	private final boolean allowUnverifiedHost;
	private final boolean enableLogging;

	private static final int BUFFER_CHAR_SIZE = 1024;

	/**
	 * Constructor
	 * 
	 * @param declaredType
	 *            The class of the object type that is fetched
	 * @param allowUnverifiedHost
	 *            Set to true to allow connecting to unverified HTTPS hosts (not
	 *            recommended)
	 * @param enableLogging
	 *            Set to true to enable the logging of outgoing and incoming
	 *            data (false is faster)
	 * @throws SAXException
	 * @throws JAXBException
	 * @throws IOException
	 */
	public MuniHTTPClient(Class<T> declaredType, boolean allowUnverifiedHost, boolean enableLogging) throws SAXException, JAXBException, IOException {
		SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
		Schema schema = factory.newSchema(new StreamSource(this.getClass().getClassLoader().getResourceAsStream("META-INF/muniws.xsd")));
		JAXBContext ctx = JAXBContext.newInstance(declaredType);

		marshaller = ctx.createMarshaller();
		marshaller.setSchema(schema);

		// Create the unmarshaller
		unmarshaller = ctx.createUnmarshaller();
		unmarshaller.setSchema(schema);

		this.allowUnverifiedHost = allowUnverifiedHost;
		this.enableLogging = enableLogging;
	}

	/**
	 * Calls the webservice and returns a HTTPResponse Object
	 * 
	 * @param urlStringBase
	 * @param resourcePath
	 * @param user
	 * @param pass
	 * @param httpMethod
	 * @param data
	 * @param declaredType
	 * @return MuniHTTPResponse
	 * @throws SAXException
	 * @throws JAXBException
	 * @throws IOException
	 */
	public MuniHTTPResponse<T> request(String urlStringBase, String resourcePath, String user, String pass, String httpMethod, T data, Class<T> declaredType) throws SAXException, JAXBException, IOException {
		// Authentication String
		String authStr = user + ":" + pass;
		String authEncoded = new sun.misc.BASE64Encoder().encode(authStr.getBytes());

		// Construct the url based on the urlStringBase and the resourcePath
		URL url = new URL("https://" + urlStringBase + "/webservices/v1/" + resourcePath);

		// Add $format to URL
		if (url.getQuery() == null) {
			url = new URL(url.toExternalForm() + "?$format=xml");
		} else {
			url = new URL(url.toExternalForm() + "&$format=xml");
		}

		// Create the connection
		HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
		MuniHTTPResponse<T> httpResponse = new MuniHTTPResponse<T>();
		try {
			if (allowUnverifiedHost) {
				connection.setHostnameVerifier(new CustomizedHostnameVerifier());
			}
			connection.setRequestMethod(httpMethod);
			connection.setRequestProperty("Authorization", "Basic " + authEncoded);

			// Marshal the data, if needed
			if (data != null) {
				connection.setDoOutput(true);

				if (enableLogging) {
					this.marshalAndLog(connection.getOutputStream(), data);
				} else {
					this.marshal(connection.getOutputStream(), data);
				}
			}

			// Unmarshal if there is a response
			InputStream inputStream = new BufferedInputStream(connection.getInputStream());

			boolean tryUnmarshal = true;

			inputStream.mark(1);
			final int bytesRead = inputStream.read(new byte[1]);
			inputStream.reset();

			if (bytesRead == -1)
				tryUnmarshal = false;

			if (tryUnmarshal) {
				if (enableLogging) {
					this.unmarshalAndLog(inputStream, httpResponse);
				} else {
					this.unmarshal(inputStream, httpResponse);
				}
			}

		} catch (IOException e) {

			Writer writer = new StringWriter();

			char[] buffer = new char[BUFFER_CHAR_SIZE];
			try {
				Reader reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
				int n;
				while ((n = reader.read(buffer)) != -1) {
					writer.write(buffer, 0, n);
				}
			} finally {
				if (connection.getErrorStream() != null)
					connection.getErrorStream().close();
			}

			httpResponse.setHttpError(writer.toString());
		}

		httpResponse.setHttpStatus(connection.getResponseCode());
		return httpResponse;
	}

	@SuppressWarnings("unchecked")
	protected void unmarshal(InputStream inputStream, MuniHTTPResponse<T> httpResponse) throws JAXBException {
		httpResponse.setData((T) unmarshaller.unmarshal(inputStream));
	}

	@SuppressWarnings("unchecked")
	protected void unmarshalAndLog(InputStream inputStream, MuniHTTPResponse<T> httpResponse) throws IOException, JAXBException {

		// write the inputStream to a FileOutputStream
		File responseFile = new File("output.xml");
		OutputStream out = new FileOutputStream(responseFile);

		int read = 0;
		byte[] bytes = new byte[BUFFER_CHAR_SIZE];

		while ((read = inputStream.read(bytes)) != -1) {
			out.write(bytes, 0, read);
		}

		out.flush();
		out.close();

		// Unmarshal the response
		httpResponse.setData((T) unmarshaller.unmarshal(responseFile));
	}

	protected void marshalAndLog(OutputStream outputStream, T data) throws JAXBException, FileNotFoundException {
		this.marshal(outputStream, data);
		this.marshal(new FileOutputStream("data.xml"), data);
	}

	protected void marshal(OutputStream outputStream, T data) throws JAXBException {
		if (data != null) {
			marshaller.marshal(data, outputStream);
		}
	}

	/**
	 * Overrides the check on a valid SSL, should only be used for development
	 * purposes and not in a live environment
	 */
	private static class CustomizedHostnameVerifier implements HostnameVerifier {
		public boolean verify(String hostname, SSLSession session) {
			return true;
		}
	}
}
